# -*- coding: utf-8 -*-
import ast
import sys
import time
import math
import random
import logging
import datetime
import pandas as pd
import multiprocessing
from collections import OrderedDict
from deap import base, tools
from lbs_service import get_travel_time
"""
将订单编号作为基因，手艺人作为种群
只做订单数量对等交换(Geatpy版)
通过交换固定位置的订单编号，搜索更优解
种群基因形式(首位为手艺人编号，-1代表不可用时间)：
[1, -1, 0, 157, 157, 157, 157, 157, 0, 412, 412, 412, 412, 412, 0, 640, 640, 640, 640, 640, 0, 908, 908, 908, 908, 0]
[53, 0, 0, 164, 164, 164, 164, 0, 0, 418, 418, 418, 418, 0, 0, 646, 646, 646, 646, 0, 0, 0, 0, 0, 0, -1]
抽取手艺人的开放时间，需要对关闭时间做检查和限制
（去掉关闭和不可用时间）
"""
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d: %(message)s')
logger = logging.getLogger(__name__)


def get_yesterday(diff_days=1):
    today = datetime.date.today()
    one_day = datetime.timedelta(days=diff_days)
    ret_day = today - one_day
    return ret_day.strftime("%Y-%m-%d")


yesterday = get_yesterday()
df_artisans = None
df_orders = None


def create_artisan():
    gene = df_artisans['genes'].values
    return gene


def get_artisan_index(se):
    artisan_index = se['genes'][0]
    return artisan_index


def time_utilization(individual):
    """
    评价个体的好坏，可观察该个体的时间利用率
    路程时间利用率 = 订单数量/路程时间
    交叉和突变时也要检查
    :param individual:
    :return:
    """
    global df_artisans, df_orders
    error_routes = False
    total_time = 0
    # 检查时间窗口是否满足
    print('check gene: ' + str(individual))
    artisan_orders = []
    artisan_id = individual[0]
    order_nums = len(set([x for x in individual[1:] if int(x) > 0]))
    for order_id in individual[1:]:
        if order_id > 0:
            if len(artisan_orders) > 0:
                pre_order_id = artisan_orders[-1]
                pre_order = df_orders.iloc[pre_order_id]
            else:
                # 如果是该手艺人第一单
                logger.debug('artisan_id:' + str(artisan_id))
                pre_order = df_artisans.iloc[artisan_id]
            now_order = df_orders.iloc[order_id]
            travel_time = 0
            if pre_order is not None:
                # 排除预约时间有冲突的情况
                try:
                    now_start = int(now_order['start'])
                except Exception as e:
                    logger.exception(e)
                    now_start = 0
                pre_time = pre_order['stop']
                if now_start < pre_time:
                    print('order[' + str(order_id) + '] service time ' + str(now_start) + ' < artisan[' + str(
                        artisan_id) + '] pre time ' + str(pre_time))
                    error_routes = True
                    break
                # 排除路程时间不够的情况
                try:
                    p1 = str(pre_order['lon']) + ',' + str(pre_order['lat'])
                    p2 = str(now_order['lon']) + ',' + str(now_order['lat'])
                    # 开始计算时间为全天，所以这里不应从9点开始
                    # start = str(math.floor((now_order['start']/(60 * 60) - 1) + 9)) + ":00"
                    start_hour = math.floor(now_order['start'] / (60 * 60) - 1)
                    if start_hour < 0:
                        start_hour = 0
                    start = str(start_hour) + ":00"
                    interval_time = int(now_order['start']) - int(pre_order['stop'])
                    travel_time = get_travel_time(now_order['city_id'], p1, p2, start)
                    logger.debug('get travel_time: ' + str(travel_time))
                except Exception as e:
                    logger.exception(e)
                    travel_time = 0
                if travel_time > interval_time:
                    logger.debug(
                        'order[' + str(order_id) + '] interval time ' + str(interval_time) + ' < artisan[' + str(
                            artisan_id) + '] travel time ' + str(travel_time))
                    error_routes = True
                    break
            total_time += travel_time
    if error_routes:
        # print('error routes')
        return None, 0
    if total_time > 0:
        utilization = order_nums / total_time
        logger.debug('find one feasible solution, fitness: ' + str(utilization))
    else:
        utilization = None
    return utilization, order_nums


def eval_vrptw(individual):
    """
    进化过程评价
    :param individual: 个体对象
    :param instance: 个体相关数据
    :return:
    """
    usage, order_nums = time_utilization(individual)
    if usage is None:
        usage = 0
    return [usage, order_nums]


class IndividualFitness(base.Fitness):
    weights = (1,)

    def __init__(self, *args, **kwargs):
        base.Fitness.__init__(self, *args, **kwargs)


def drop_duplicate(ind_list):
    distinct_list = []
    for i in ind_list:
        if i not in distinct_list:
            distinct_list.append(i)
    return distinct_list


def remove_dict_value(dic, v):
    new_dic = {k: d for k, d in dic.items() if d != v}
    return new_dic


def genes_to_align(ind1, ind2):
    """
    基因二次编码
    步骤：
     1.去掉同为0部分
     2.左遍历，尾对齐
     3.右遍历，首对齐
     4.去重并去除不合法项
    :param ind1:
    :param ind2:
    :return:
    """
    # 1.去掉同为0部分
    # [1, 1, 2, 0, 0, 3, 3, 7, 9, 0, 10, 10]
    # [4, 0, 5, 5, 6, 6, 0, 0, 8, 8, 8, 0]
    try:
        i1_align, i2_align = zip(*[(i1, i2) for i1, i2 in zip(ind1, ind2) if (i1 + i2) > 0])
        i1_align = list(i1_align)
        i2_align = list(i2_align)
    except Exception as e:
        i1_align = [0]
        i2_align = [0]
        # logger.debug(ind1)
        # logger.debug(ind2)
        logger.exception(e)

    # 2.左遍历，尾对齐
    # [1,1,2,2,0,3,3,7,9,9,10,10]
    # [4,4,5,5,6,6,6,0,8,8,8,8]
    i1_pre_dna = 0
    i2_pre_dna = 0
    for i in range(len(i1_align)):
        if i1_align[i] == i1_pre_dna or i2_align[i] == i2_pre_dna:
            if i1_align[i] == 0:
                i1_align[i] = i1_pre_dna
            if i2_align[i] == 0:
                i2_align[i] = i2_pre_dna
        # print("i1_align[" + str(i) + "]=" + str(i1_align[i]) + " i1_pre_dna=" + str(i1_pre_dna))
        # print("i2_align[" + str(i) + "]=" + str(i2_align[i]) + " i2_pre_dna=" + str(i2_pre_dna))
        i1_pre_dna = i1_align[i]
        i2_pre_dna = i2_align[i]

    # 3.右遍历，首对齐
    # [1, 1, 2, 2, 3, 3, 3, 7, 9, 9, 10, 10]
    # [4, 4, 5, 5, 6, 6, 6, 0, 8, 8, 8, 8]
    i1_pre_dna = 0
    i2_pre_dna = 0
    for i in range(len(i1_align) - 1, -1, -1):
        if i1_align[i] == i1_pre_dna or i2_align[i] == i2_pre_dna:
            if i1_align[i] == 0:
                i1_align[i] = i1_pre_dna
            if i2_align[i] == 0:
                i2_align[i] = i2_pre_dna
        i1_pre_dna = i1_align[i]
        i2_pre_dna = i2_align[i]

    # 4.去重并去除不合法项
    check_dict = {}
    duplicate_keys = []
    duplicate_values = []
    check_set = OrderedDict.fromkeys((i1, i2) for i1, i2 in zip(i1_align, i2_align)).keys()
    for o in list(check_set):
        if o[0] * o[1] != 0:
            # print(o[0], o[1])
            # 如果键已存在，说明是1对多关系，删除
            if str(o[0]) in check_dict.keys():
                duplicate_keys.append(str(o[0]))
            # 如果值已存在，同上
            elif o[1] in check_dict.values():
                duplicate_values.append(o[1])
            check_dict[str(o[0])] = o[1]
    for d in duplicate_keys:
        del check_dict[d]
    for d in duplicate_values:
        check_dict = remove_dict_value(check_dict, d)

    return list(check_dict.keys()), list(check_dict.values())


def align_to_genes(aligned, no_work_index, order_index, order_hour):
    """
    二次编码解码
    :param aligned: [1, 2, 3]
    :param no_work_index: [0,1,2,3,24,25,45,46,47] 非工作时间
    :param order_index: {'1': 12, '2': 18, '3': 28}
    :param order_hour: {'1': 2, '2': 3, '3': 3}
    :return: [-1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, -1, -1, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1]
    """
    gens = []
    order_id = 0
    repeat = 0
    # 开始组装
    for half_hour in range(24 * 2):
        if half_hour in no_work_index:
            code = -1
        elif repeat > 0:
            code = order_id
            repeat -= 1
        else:
            code = 0
            # 取出当前时间段的订单
            orders = [k for k, v in order_index.items() if v == half_hour]
            if orders is not None and len(orders) > 0:
                for a in aligned:
                    if str(a) in orders:
                        repeat = order_hour[str(a)] - 1
                        order_id = int(a)
                        code = order_id
                        break
        gens.append(code)
    if len(gens) != 48:
        logger.debug('gen genes length:' + str(len(gens)))
        logger.debug(gens)
    return gens


def get_no_work_index(ind):
    """
    取得不可交换的关闭时间
    :return:
    """
    try:
        no_work_index = [i for i in range(len(ind) - 1) if ind[i + 1] == -1]
    except:
        no_work_index = []
    return no_work_index


def get_work_index(ind1, ind2):
    """
    取得可交换的开放时间
    :param ind1:
    :param ind2:
    :return:
    """
    try:
        no_work_index = [i for i in range(len(ind1) - 1) if ind1[i + 1] == -1 or ind2[i + 1] == -1]
    except:
        no_work_index = []
    work_ind1 = [order_id for i, order_id in enumerate(ind1[1:]) if i not in no_work_index]
    work_ind2 = [order_id for i, order_id in enumerate(ind2[1:]) if i not in no_work_index]
    return work_ind1, work_ind2


def get_order_index(work_ind):
    """
    取得订单位置
    :param work_ind:
    :return:
    """
    o_index = {}
    if work_ind is not None:
        o_index = {str(order_id): i for i, order_id in enumerate(work_ind) if order_id > 0}
    return o_index


def get_order_hour(work_ind):
    """
    取得订单长度
    :param work_ind:
    :return:
    """
    o_hour = {}
    if work_ind is not None:
        o_hour = {str(order_id): work_ind.count(order_id) for i, order_id in enumerate(work_ind) if order_id > 0}
    return o_hour


def order_time_matched(ind1, ind2):
    """
    交叉方法
    手艺人订单基因中的订单时间必须匹配，才能交换变异
    0.首先排除两个个体不可用时间
    目前只考虑1种可交换的情况：
    1.个体时间对应：个体1与个体2中的订单占用时间可交换
    原始基因：
    [100,-1,1,0,2,0,0,0,3,3,0,7,0,9,0,10,10]
    [104,-1,0,0,5,5,0,6,6,0,0,0,0,8,8,8,0]
    二次编码
    可用于交换变异的基因别名：
    [2,3]
    [5,6]
    :param ind1:
    :param ind2:
    :return:
    """
    logger.debug('ind1: ' + str(len(ind1)) + ' ind2: ' + str(len(ind2)))
    no_work_index1 = get_no_work_index(ind1)
    no_work_index2 = get_no_work_index(ind2)
    if len(ind1) > 1 and len(ind2) > 1:
        # 用字典记住订单的位置和长度，方便解码
        work_ind1, work_ind2 = get_work_index(ind1, ind2)
        # 订单位置：order_index: {'1': 1, '2': 4, '3': 8}
        # 订单长度：order_hour: {'1': 2, '2': 1, '3': 2}
        order_ind = work_ind1.extend(work_ind2)
        order_index = get_order_index(order_ind)
        order_hour = get_order_hour(order_ind)
        i1_aligned, i2_aligned = genes_to_align(work_ind1, work_ind2)
        if len(i1_aligned) >= 1 and len(i1_aligned) == len(i2_aligned):
            # logger.debug(i1_aligned)
            # logger.debug(i2_aligned)
            # 使用基因别名实现 均匀交叉（不会改变DNA位置）
            ind_prob = 1 / len(i1_aligned)
            i1, i2 = tools.cxUniform(i1_aligned, i2_aligned, ind_prob)
            # logger.debug('genes mated:')
            # logger.debug(i1)
            # logger.debug(i2)
            ind1[1:] = align_to_genes(i1, no_work_index1, order_index, order_hour)
            ind2[1:] = align_to_genes(i2, no_work_index2, order_index, order_hour)
        # logger.debug('new gens:')
        # logger.debug(ind1)
        # logger.debug(ind2)
    return ind1, ind2


def no_mutate(individual):
    # new_individual = tools.mutShuffleIndexes(individual, indpb = mut_pb)
    return individual


def ga_vrptw(pool_size, pop_size, cx_pb, gen_size):
    """
    遗传算法主程序
    :param pool_size: 线程池数量
    :param pop_size: 种群数量
    :param cx_pb: 交叉概率
    :param gen_size: 进化次数
    :return:
    """
    toolbox = base.Toolbox()
    if pool_size <= 0:
        pool = multiprocessing.Pool()
    else:
        pool = multiprocessing.Pool(pool_size)
    toolbox.register("map", pool.map)
    # 定义种群生成器
    # 二次派单问题中，我们不需要随机生成N个种群，只需要在现有固定接单手艺人的基础上交叉变异即可
    # toolbox.register('population', tools.initRepeat, list, toolbox.individual)
    toolbox.register('population', create_artisans)
    # 生成种群
    pop = toolbox.population(n=pop_size)
    # pop = create_artisans()
    # print(pop)
    # 定义评价方法
    toolbox.register('evaluate', eval_vrptw)
    # 定义选择方法
    # toolbox.register('select', tools.selRoulette)
    # 每次选择最差配种
    toolbox.register('select', tools.selWorst)
    # 定义交叉方法
    toolbox.register('mate', order_time_matched)
    # 定义突变方法
    # 订单时间位置特殊，突变会导致时间窗口被破坏，不进行突变
    toolbox.register('mutate', no_mutate)
    # Begin the evolution
    g = 0
    fitnesses = list(toolbox.map(toolbox.evaluate, pop))
    logger.debug("fitnesses" + str(fitnesses))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit
    fits = [ind.fitness.values[0] for ind in pop]
    # order_nums = [ind.fitness.values[0][1] for ind in pop]
    best_fit = sum(fits)
    # best_order = sum(order_nums)
    best_fits = [best_fit]
    best_solution = []
    # 第一代种群中 未找到有效派单方案
    if len(fits) == 0:
        return "no feasible solution found, please increase population size."
    while len(fits) > 0 and g < gen_size:
        # A new generation
        g = g + 1
        print("-- Generation %i --" % g)
        # Select the next generation individuals
        offspring = toolbox.select(pop, len(pop))
        # 保留未选择的父代个体
        pre_pop = [x for x in pop if x not in offspring]
        # Clone the selected individuals
        offspring = list(map(toolbox.clone, offspring))
        print("select " + str(len(offspring)) + " individuals")
        print("remain " + str(len(pre_pop)) + " individuals")
        # Apply crossover and mutation on the offspring
        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            # cross two individuals with probability CXPB
            if random.random() < cx_pb:
                toolbox.mate(child1, child2)
                # fitness values of the children
                # must be recalculated later
                del child1.fitness.values
                del child2.fitness.values
        # Evaluate the individuals with an invalid fitness
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = map(toolbox.evaluate, invalid_ind)
        for ind, fit in zip(invalid_ind, fitnesses):
            ind.fitness.values = fit
        print("  Evaluated %i individuals" % len(invalid_ind))
        # The population is entirely replaced by the offspring
        pop = list(offspring)
        pop.extend(pre_pop)
        # Gather all the fitnesses in one list and print the stats
        fits = [ind.fitness.values[0] for ind in pop]
        # order_nums = [ind.fitness.values[0][1] for ind in pop]
        if best_fit < sum(fits):
            best_fit = sum(fits)
            best_fits.append(best_fit)
            # best_order = sum(order_nums)
            best_solution = pop
    pool.close()
    print("-- End of (successful) evolution --")
    # 此处不同于传统的进化算法挑选个体
    # 而是挑选进化过程中得分最高的某一代
    if best_fit > 0:
        print("Best population is %s, %s" % (best_solution, best_fit))
        # print("save travel_times %d " % (best_order / best_fit))
        best_df = pd.DataFrame({'genes': best_solution})
        if len(best_df) > 0:
            best_df['artisan_id'] = best_df.apply(get_artisan_index, axis=1)
            best_df.sort_values('artisan_id', inplace=True)
            best_df.to_csv('/data/disp/' + yesterday + '/best_result.csv')
        eval_df = pd.DataFrame({'fits': best_fits})
        eval_df.to_csv('/data/disp/' + yesterday + '/eval_fits.csv')


def train(pool_size, gen_size=5):
    """
    进化主程序
    :param pool_size:
    :param gen_size:
    :return:
    """
    global df_artisans
    df_artisans['genes'] = df_artisans['genes'].apply(create_individual)
    pop_size = len(df_artisans)
    # 交叉概率(意味着50%手艺人的订单会发生交叉)
    cx_pb = 0.5
    # Global creation of the individuals for GA
    ga_vrptw(
        pool_size=pool_size,
        pop_size=pop_size,
        cx_pb=cx_pb,
        gen_size=gen_size
    )


if __name__ == '__main__':
    # 北京、上海、杭州、深圳
    # 分城市+类目
    try:
        city_id = sys.argv[1]
        cate_id = sys.argv[2]
    except Exception as e:
        city_id = 0
        cate_id = 0
    df_artisans = pd.read_csv('/data/disp/' + yesterday + '/artisans_' + str(city_id) + '_' + str(cate_id) + '.csv')
    df_orders = pd.read_csv('/data/disp/' + yesterday + '/orders_' + str(city_id) + '_' + str(cate_id) + '.csv')
    tic = time.clock()
    train(28, 5)
    # train(0, 10)
    print('Computing Time: %s' % (time.clock() - tic))
